package plugin.demo.shardingsphere.seata.config.v5_0_0;

import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.shardingsphere.driver.api.ShardingSphereDataSourceFactory;
import org.apache.shardingsphere.infra.config.RuleConfiguration;
import org.apache.shardingsphere.infra.config.algorithm.ShardingSphereAlgorithmConfiguration;
import org.apache.shardingsphere.replicaquery.api.config.ReplicaQueryRuleConfiguration;
import org.apache.shardingsphere.replicaquery.api.config.rule.ReplicaQueryDataSourceRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.ShardingRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.rule.ShardingTableRuleConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.keygen.KeyGenerateStrategyConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.sharding.HintShardingStrategyConfiguration;
import org.apache.shardingsphere.sharding.api.config.strategy.sharding.StandardShardingStrategyConfiguration;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import plugin.demo.shardingsphere.seata.config.proxy.ShardingSphereDataSourceProxy;
import tk.mybatis.spring.annotation.MapperScan;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.function.Function;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2021/4/22 上午11:58
 * @UpdateDate:
 * @Description:
 */
@Configuration
@MapperScan(basePackages = "plugin.demo.shardingsphere.mappers",
        sqlSessionFactoryRef = "sqlSessionFactory")
@Slf4j
public class ShardingJDBC5_0_0Adaptor_Hignly {

    private Map<String,DataSource> writeDbs(Map<String,DataSource> dataSourceMap){
        /*
         // HikariDataSource dataSource1 = new HikariDataSource();
        注意这里创建数据源的方式为匿名类，创建后数据源名称为:
        authentication.config.sharding.config.jdbc.v5_0_0.enhancer.HikariEnhancerPropertyProviders$HikariEnhancer$1
        而并非：com.zaxxer.hikari.HikariDataSource
        而ShardingSphere-JDBC 分布式事务XA是JTA的一套实现(JTA将任意类型的数据源封装成支持分布式事务的数据源)，
        采用SPI技术针对不同数据源引入数据源装饰者Provider，顶层抽象为:
        org.apache.shardingsphere.transaction.xa.jta.datasource.swapper.DataSourcePropertyProvider,
        因此需要采用SPI的技术为改写后的匿名数据源提供装饰者读取数据源核心属性的Provider，
        该Provider除了getDataSourceClassName(JTA用于获取数据源的实现，进行适配与装饰)需要返回匿名类
        全名外，其内核实际是HikariCP(其余属性与 HikariCPPropertyProvider一致即可).
        Question: 为什么这样做，非要用匿名内部类改写DataSource?
                answers: 在日益复杂的数据分片以及路由中，使用ShardingSphere默认的日志输出(dataNode,logicTale,logicSQL,actualSQL)
                         愈发不能直观的看到本次数据库DML/DQL的路由情况，而采用方法重新的方式(内部类)/数据库代理的方式(Enhancer) 去更好的
                         做数据库日志，连接日志等等，就需要这样做了~
        * */
        HikariDataSource dataSource1 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("写操作 [dn0] : 118.31.13.167:3306/test");
                return super.getConnection();
            }
        };
        dataSource1.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource1.setJdbcUrl("jdbc:mysql://118.31.13.167:3306/test?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource1.setUsername("root");
        dataSource1.setPassword("root");
        dataSourceMap.put("dn0", dataSource1);

        HikariDataSource dataSource1_1 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("写操作 [dn1] : 118.31.13.167:3306/test2");
                return super.getConnection();
            }
        };
        dataSource1_1.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource1_1.setJdbcUrl("jdbc:mysql://118.31.13.167:3306/test2?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource1_1.setUsername("root");
        dataSource1_1.setPassword("root");
        dataSourceMap.put("dn1", dataSource1_1);

        HikariDataSource dataSource1_2 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("写操作 [dn2] : 118.31.13.167:3306/test3");
                return super.getConnection();
            }
        };
        dataSource1_2.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource1_2.setJdbcUrl("jdbc:mysql://118.31.13.167:3306/test3?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource1_2.setUsername("root");
        dataSource1_2.setPassword("root");
        dataSourceMap.put("dn2", dataSource1_2);

        return dataSourceMap;
    }


    private Map<String,DataSource> readDb0(Map<String,DataSource> dataSourceMap){

        // 配置第 2 个数据源 dn2:3307 -> test,test2,test3
        HikariDataSource dataSource2 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave0_test] : 118.31.13.167:3307/test");
                return super.getConnection();
            }
        };
        dataSource2.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource2.setJdbcUrl("jdbc:mysql://118.31.13.167:3307/test?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource2.setUsername("root");
        dataSource2.setPassword("root");
        dataSourceMap.put("dn0", dataSource2);

        HikariDataSource dataSource2_1 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave0_test2] : 118.31.13.167:3307/test2");
                return super.getConnection();
            }
        };
        dataSource2_1.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource2_1.setJdbcUrl("jdbc:mysql://118.31.13.167:3307/test2?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource2_1.setUsername("root");
        dataSource2_1.setPassword("root");
        dataSourceMap.put("dn1", dataSource2_1);

        HikariDataSource dataSource2_2 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave0_test3] : 118.31.13.167:3307/test3");
                return super.getConnection();
            }
        };
        dataSource2_2.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource2_2.setJdbcUrl("jdbc:mysql://118.31.13.167:3307/test3?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource2_2.setUsername("root");
        dataSource2_2.setPassword("root");
        dataSourceMap.put("dn2", dataSource2_2);

        return dataSourceMap;
    }

    private Map<String,DataSource> readDb1(Map<String,DataSource> dataSourceMap){

        // 配置第 3 个数据源 dn3:3308 -> test,test2,test3
        HikariDataSource dataSource3 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave1_test] : 118.31.13.167:3308/test");
                return super.getConnection();
            }
        };
        dataSource3.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource3.setJdbcUrl("jdbc:mysql://118.31.13.167:3308/test?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource3.setUsername("root");
        dataSource3.setPassword("root");
        dataSourceMap.put("dn0", dataSource3);

        HikariDataSource dataSource3_1 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave1_test2] : 118.31.13.167:3308/test2");
                return super.getConnection();
            }
        };
        dataSource3_1.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource3_1.setJdbcUrl("jdbc:mysql://118.31.13.167:3308/test2?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource3_1.setUsername("root");
        dataSource3_1.setPassword("root");
        dataSourceMap.put("dn1", dataSource3_1);

        HikariDataSource dataSource3_2 = new HikariDataSource(){
            @Override
            public Connection getConnection() throws SQLException {
                log.info("读操作 [slave1_test3] : 118.31.13.167:3308/test3");
                return super.getConnection();
            }
        };
        dataSource3_2.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource3_2.setJdbcUrl("jdbc:mysql://118.31.13.167:3308/test3?userSSL=true&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT");
        dataSource3_2.setUsername("root");
        dataSource3_2.setPassword("root");
        dataSourceMap.put("dn2", dataSource3_2);

        return dataSourceMap;
    }

    private ShardingRuleConfiguration tableConfigInit(){
        // 配置 逻辑表【test_order -> test_order_*】 表规则
        ShardingTableRuleConfiguration orderTableRuleConfig =
                new ShardingTableRuleConfiguration("test_order", "dn${0..2}.test_order_${0..1}");
        orderTableRuleConfig.setDatabaseShardingStrategy(
                new StandardShardingStrategyConfiguration("uid", "dbRule:test_order_*@[uid]")
        );
        orderTableRuleConfig.setTableShardingStrategy(
                new StandardShardingStrategyConfiguration("order_id", "tableRule:test_order_*@[order_id]")
        );
        orderTableRuleConfig.setKeyGenerateStrategy(new KeyGenerateStrategyConfiguration("cls_desc","keyGeneric:test_order_*@[cls_desc]"));


        // 配置 order_item 表规则
        ShardingTableRuleConfiguration orderItemRuleConfig =
                new ShardingTableRuleConfiguration("order_item","dn${0..2}.order_item_${0..1}");
        orderItemRuleConfig.setDatabaseShardingStrategy(
                new StandardShardingStrategyConfiguration("uid","dbRule:order_item_*@[uid]")
        );
        orderItemRuleConfig.setTableShardingStrategy(
                new StandardShardingStrategyConfiguration("order_id","tableRule:order_item_*@[order_id]")
        );

        // volume_range 路由规则.
        ShardingTableRuleConfiguration volumeRangeRuleConfig =
                new ShardingTableRuleConfiguration("volume_range","dn${0..2}.volume_range_${0..1}");
        volumeRangeRuleConfig.setDatabaseShardingStrategy(
                new StandardShardingStrategyConfiguration("id","dbRule:volume_range_*@[id]")
        );
        volumeRangeRuleConfig.setTableShardingStrategy(
                new StandardShardingStrategyConfiguration("range_key","tableRule:volume_range_*@[range_key]")
        );
        volumeRangeRuleConfig.setKeyGenerateStrategy(new KeyGenerateStrategyConfiguration("sknow_flake","keyGeneric:volume_range_*@[sknow_flake]"));

        // hint_tab 路由规则
        ShardingTableRuleConfiguration hintTabRuleConfig =
                new ShardingTableRuleConfiguration("hint_tab","dn${0..2}.hint_tab_${0..1}");
        hintTabRuleConfig.setDatabaseShardingStrategy(new HintShardingStrategyConfiguration("dbRule:hint_tab_*@[*]"));
        hintTabRuleConfig.setTableShardingStrategy(new HintShardingStrategyConfiguration("tableRule:hint_tab_*@[*]"));
        hintTabRuleConfig.setKeyGenerateStrategy(new KeyGenerateStrategyConfiguration("hint_key","keyGeneric:hint_tab_*@[hint_key]"));

        // 配置所有数据库表 的 分片路由规则.
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTables().add(orderTableRuleConfig);
        shardingRuleConfig.getTables().add(orderItemRuleConfig);
        shardingRuleConfig.getTables().add(volumeRangeRuleConfig);
        shardingRuleConfig.getTables().add(hintTabRuleConfig);

        /*配置所有数据库表的 分片算法 合集.*/
        //test_order 数据库路由 与 表路由。数据库 uid % 3 , 表 order_id % 2 (test_order 表 只有 0,1)
        shardingRuleConfig.getShardingAlgorithms().put("dbRule:test_order_*@[uid]",new ShardingSphereAlgorithmConfiguration("MOD", new Properties(){{
            setProperty("sharding-count", "3");
        }}));
        shardingRuleConfig.getShardingAlgorithms().put("tableRule:test_order_*@[order_id]",new ShardingSphereAlgorithmConfiguration("MOD", new Properties(){{
            setProperty("sharding-count", "2");
        }}));

        //order_item 数据库路由 与 表路由，数据库保持跟 test_order一致(订单详情所落数据库一定是订单落下的数据库)，表 order_item_id % 3 因为order_item 数据量大，存在3张分片表, 表路由为聚合策略 uid, order_item_id
        shardingRuleConfig.getShardingAlgorithms().put("dbRule:order_item_*@[uid]",new ShardingSphereAlgorithmConfiguration("INLINE", new Properties(){{
            setProperty("algorithm-expression", "dn${uid % 3}");
        }}));
        shardingRuleConfig.getShardingAlgorithms().put("tableRule:order_item_*@[order_id]",new ShardingSphereAlgorithmConfiguration("MOD", new Properties(){{
            setProperty("sharding-count", "2");
        }}));

        // volume_range 路由到数据库 0,1 上
        shardingRuleConfig.getShardingAlgorithms().put("dbRule:volume_range_*@[id]",new ShardingSphereAlgorithmConfiguration("STANDARD_EL_PARSER", new Properties(){{
            setProperty("el-expression", " #id % 3");
            //setProperty("monotonicity", "++");  无单调性.
        }}));
        shardingRuleConfig.getShardingAlgorithms().put("tableRule:volume_range_*@[range_key]",new ShardingSphereAlgorithmConfiguration("STANDARD_EL_PARSER", new Properties(){{
            setProperty("el-expression", " #range_key <= 10 ? 0 : 1");
            setProperty("monotonicity", "++");  //表路由具备单调性 ++
        }}));

        // hint_tab 路由算法
        shardingRuleConfig.getShardingAlgorithms().put("dbRule:hint_tab_*@[*]",new ShardingSphereAlgorithmConfiguration("HINT_EL_PARSER",new Properties(){{
            setProperty("db","db");
        }}));
        shardingRuleConfig.getShardingAlgorithms().put("tableRule:hint_tab_*@[*]",new ShardingSphereAlgorithmConfiguration("HINT_EL_PARSER",new Properties(){{
            setProperty("tab","tab");
        }}));



        /*key生成算法*/
        KeyGenerateStrategyConfiguration defaultKeyGenerateStrategy = shardingRuleConfig.getDefaultKeyGenerateStrategy();
        shardingRuleConfig.getKeyGenerators().put("keyGeneric:hint_tab_*@[hint_key]",new ShardingSphereAlgorithmConfiguration("SNOWFLAKE",new Properties(){{
            setProperty("worker-id","0");
        }}));
        shardingRuleConfig.getKeyGenerators().put("keyGeneric:test_order_*@[cls_desc]",new ShardingSphereAlgorithmConfiguration("UUID",new Properties(){{
            setProperty("casue","uuid");
        }}));
        Function<UUID,String> x_uuid_processor = (uuid) -> {
            return uuid.toString().toUpperCase();
        };
        shardingRuleConfig.getKeyGenerators().put("keyGeneric:volume_range_*@[sknow_flake]",new ShardingSphereAlgorithmConfiguration("X_UUID",new Properties(){{
            put("processor",x_uuid_processor);
        }}));

        /*配置所有数据库表 的分片绑定, 在开发中不一般不建议绑定表，经测试，表一旦绑定后路由大概率可能出现问题，
        例如left join联查，主表路由到dn1.tab1 和 dn1.tab2, 但是关联表的路由可能配置不通，例如订单和详情，
        详情表的数据量可能为订单表的3-5倍，因此订单分表为dn1-3均分出order_0,order_1,而详情表在dn1-3均分出
         order_item_0,order_item_1,order_item_2，路由配置不同，一旦绑定了订单和详情的逻辑表，而在关联查询时，
         库表的路由均按照主表(order_*)来，例如dn1.order_1 left join dn1.order_item_1, 而忽略了order_item_0以及order_item_1
         ，因此在数据分片规则不同时，使用逻辑关联，智能满足很小一部分查询逻辑(大多数情况下会由于路由配置的差异导致查询错误或者不完整)，
         因此，慎用！,或者使用HintManager去强制控制路由~*/
        //shardingRuleConfig.getBindingTableGroups().add("test_order,order_item");

        return shardingRuleConfig;
    }


    @Bean("dataSource")
    public DataSource dataSource() throws SQLException {

        Map<String,DataSource> aggregate = new HashMap<>();

        //系统配置
        Properties globalProps = new Properties();
        globalProps.setProperty("sql-show","true");

        ShardingRuleConfiguration tabConfigs = tableConfigInit();
        DataSource aggregate_dn0 = ShardingSphereDataSourceFactory.createDataSource(
                writeDbs(new HashMap<>()),
                Arrays.asList(tabConfigs),
                globalProps
        );
        aggregate.put("master",aggregate_dn0);

        DataSource aggregate_dn1 = ShardingSphereDataSourceFactory.createDataSource(
                readDb0(new HashMap<>()),
                Arrays.asList(tabConfigs),
                globalProps
        );
        aggregate.put("slave0",aggregate_dn1);

        DataSource aggregate_dn2 = ShardingSphereDataSourceFactory.createDataSource(
                readDb1(new HashMap<>()),
                Arrays.asList(tabConfigs),
                globalProps
        );
        aggregate.put("slave1",aggregate_dn2);

        //主从.
        RuleConfiguration masterSlaveConfig = getMasterSlaveConfig();
        DataSource dataSource = ShardingSphereDataSourceFactory.createDataSource(aggregate, Collections.singleton(masterSlaveConfig), globalProps);

        /**
         * Notice:
         * 1.需要注意的一点是经过二次代理的ShardingSphereDataSource被final修饰，
         * 无法被SeataDataSourceProxy的修饰器进行再代理(Cglib),
         * 因此对于二采代理后的数据源需要手动再代理一次 :ShardingSphereDataSourceProxy.
         *
         * 2.registry.conf 以及 file.conf 是 alibaba io.seata的配置信息，使用这种方式仅需要使用io.seata-all的客户端依赖，
         * 进行配置读取解析即可，且file.conf 由registry.conf的conf模块加载，
         * 而seata.conf 是ShardingSphere-JDBC分布式事务BASE模式下适配alibaba seata的配置信息(并非seata的配置)
         *
         * 3.使用springboot-seata / springcloud-seata应该用SeataAConfiguration进行系统参数解析(application.yml)，
         * 加载seata支持的配置内容, 实际重写了registry.conf和file.conf的内容。
         *
         * 4.注意ShardingSphere柔性事务适配 与 io.seata的版本版本冲突，
         * 使用 shardingsphere-transaction-base-seata-at: 5.0.0-alpha 应使用io.seata: 1.1.0的内核
         * (这一点上Apache ShardingSphere的适配做的还是有点儿慢的)
         *
         * 5.seata 以及 base-seata-at 客户端配置解析:
         *      registry.conf:
         *          registry: 配置seata服务端的注册方式，以及注册信息(高可用模式)，默认使用file本地模式
         *          config: 客户端读取/注册资源(数据库)/事务到seata服务器时读取的配置来源，默认使用file本地模式，
         *                  读取三方配置中心上的配置信息需要在服务端将配置信息交由三方配置中心托管
         *                  (参考## [config-center](https://github.com/seata/seata/tree/develop/script/config-center))
         *      file.conf:
         *          配置客户端网seata服务端注册资源(数据库)以及事务的配置，以及注册过程需要读取的属性Mapping.
         *      seata.conf:
         *          配置ShardingSphere-JDBC分布式事务BASE模式，适配seata内核的配置.
         *
         * 6.seata的核心是事务组的配置
         *   不通应用可将资源，事务注册到某一个事务组(事务组可配置多个)，分布式环境下服务与服务的事务协调均由该事务组所管理TC
         *
         * 7.客户端所注册的事务组必须在服务端存在(维护),
         *   客户端事务组的配置KEY为：seata.tx-service-group
         *
         * 8.由于这采用SpringBoot的seata自动配置，因此registry.conf与file.conf均没有被读取，
         *  整个注册过程的配置均由application.yml替代.
         */
        return ShardingSphereDataSourceProxy.create(dataSource);
    }


    /**
     * 主从配置组
     * @return
     */
    private RuleConfiguration getMasterSlaveConfig(){
        /*
            主从组one, 作用与主分片数据库(test,test2,test3) : MasterSlaveGroup1@test,test2,test3 ,
            主数据节点 master(3306 数据分片:dn1 -> test, dn2 -> test2, dn3 -> test3),
            从数据节点slave0(3307 数据分片:dn1 -> test, dn2 -> test2, dn3 -> test3),
            从数据节点slave1(3308 数据分片:dn1 -> test, dn2 -> test2, dn3 -> test3),
            负载算法:MasterSlaveBalancer::PERHAPS_ROUND_ROBIN, 简化 ROUND_ROBIN, 舍弃取模绝对值!
         */
        ReplicaQueryDataSourceRuleConfiguration masterSlaveGroupOne = new ReplicaQueryDataSourceRuleConfiguration(
                "MasterSlaveGroup1@test,test2,test3",
                "master",
                Arrays.asList("slave0","slave1"),
                "MasterSlaveBalancer::PERHAPS_ROUND_ROBIN"
        );

        ReplicaQueryRuleConfiguration masterSlavesConfigs = new ReplicaQueryRuleConfiguration(
                Arrays.asList(masterSlaveGroupOne),
                new HashMap<String, ShardingSphereAlgorithmConfiguration>(){{
                    put(
                            "MasterSlaveBalancer::PERHAPS_ROUND_ROBIN",
                            new ShardingSphereAlgorithmConfiguration("PERHAPS_LOAD_BALANCER_ROUND_ROBIN",new Properties())
                    );
                }}
        );

        return masterSlavesConfigs;
    }

    @Bean("sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(final DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        return sqlSessionFactoryBean.getObject();
    }

}
